Skip to content

monitor-freshness: alert when a monitor stops completing, not just when it fails - #276

Draft
bdelanghe wants to merge 1 commit into
mainfrom
claude/bounded-tools-review-kymk75
Draft

monitor-freshness: alert when a monitor stops completing, not just when it fails#276
bdelanghe wants to merge 1 commit into
mainfrom
claude/bounded-tools-review-kymk75

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

Step 1 of #258, diagnosed in this comment.

The problem

rekor-monitor.yml last completed on 2026-08-21 and was cancelled on every run for the thirteen days after. Nothing said so.

The reusable workflow's notification job fires on failure. A cancelled run is not a failed one, so the if-failed job skipped every time and the Actions tab stayed green while the Sigstore identity monitoring — the control that workflow's own header credits with catching the Shai-Hulud worm — was doing nothing at all.

The generalisable lesson is that the status of the last run cannot tell you whether a monitor is working. A monitor can be cancelled, skipped, disabled, or have its schedule quietly dropped by GitHub, and in every one of those cases the newest row still looks recent. The one signal that survives all of them is the age of the last success, which is what this checks.

What this adds

  • scripts/check-monitor-freshness.mjs — reads the newest successful run of each declared monitor and files one issue per stuck one. Dependency-free on purpose: the watcher must not be takeable down by the thing it would then fail to report. --dry-run prints the table and writes nothing.
  • .github/workflows/monitor-freshness.yml — every 6 hours, matched to the tightest declared threshold so an alert is never more than one period behind the condition. contents: read by default, issues: write only on the job, both actions SHA-pinned.
  • scripts/check-monitor-freshness.test.mjs — 21 tests, wired into npm run check.

Design notes worth review

An API failure is reported as unchecked, never as healthy. This has a regression test, because the first draft got it wrong: it swallowed a 401 and printed All monitors have completed within their thresholds, exit 0. That reproduced, inside the watcher, the exact bug the watcher exists to catch — absence of a signal read as the presence of health. A 404 is treated as a genuinely absent workflow and skipped; everything else counts as unchecked and turns the run red.

Thresholds are multiples of each monitor's own period, not round numbers. rekor-monitor is hourly and gets 6h, so five dropped or overrunning slots pass quietly; link-check is weekly and gets 10 days. GitHub drops scheduled runs under load, so a threshold at 1× the period would be noise, and noise is how a real alert gets ignored.

One issue per stuck monitor, then silence. A watcher that opens a fresh issue every six hours is its own kind of silence. The title is stable per monitor and an existing open issue suppresses re-filing.

The clock and fetch are injected, so every decision path is exercised offline against a stubbed API. A monitor's own tests must not be hostage to the service it monitors — the same reasoning link-check.yml's header already records for keeping external probes out of the build.

Not part of npm run check. That suite is hermetic and this needs the network. Its tests are in check; the check itself is a schedule, like the monitors it watches.

Verification

$ node --test scripts/legibility/coldread.test.mjs scripts/check-monitor-freshness.test.mjs
# tests 51
# pass 51
# fail 0

check-node-uniqueness and check-license both still pass. I could not exercise the live API path from my environment — no usable token — which is part of why the decision path is covered by a stubbed suite rather than a smoke run.

Out of scope, still open on #258

Deliberately not attempted here, because both need a maintainer's judgement about the security posture:

  • Re-seeding the checkpoint. The gap was already ~201.6M log entries on 08-29 and grows hourly. It needs one long dispatch with nothing competing for the slot.
  • The cadence. Today's run stopped at 59m57s, about 20 seconds before the next 41 * * * * tick. That points at concurrency cancellation rather than a timeout, since GitHub's default job timeout is 360 minutes, but I could not confirm it from the logs alone. If it is concurrency, the scan simply takes longer than the interval between scans and the interval has to move.

Note that this PR does not make rekor-monitor work. It makes the next thirteen-day outage visible on day one. Given that, Claim-issue: rather than Closes:#258 stays open.

Also worth considering, though not done here: this is generic enough to belong in bounded-systems/.github as a reusable that every repo calls, the shape claim-sweep and pr-claim already use. Kept to one repo until it has earned that.

Claim-issue: #258

🤖 Generated with Claude Code

https://claude.ai/code/session_01FQNTis6LuJHd5KDXTvYG74


Generated by Claude Code

…en it fails

rekor-monitor.yml last completed on 2026-08-21 and was cancelled on every
run for the thirteen days after (#258). Nothing said so. The reusable
workflow's notification job fires on `failure`, and a cancelled run is not
a failed one, so the Actions tab stayed green while the Sigstore identity
monitoring was doing nothing.

The status of the last run cannot answer whether a monitor is working. A
monitor can be cancelled, skipped, disabled, or have its schedule dropped
by GitHub, and the newest row still looks recent in every case. The signal
that survives all of them is the age of the last SUCCESS.

Adds scripts/check-monitor-freshness.mjs, dependency-free so the watcher
cannot be taken down by the thing it would then fail to report, plus a
6-hourly workflow that files one issue per stuck monitor and then leaves
it alone.

An API failure is reported as unchecked, never as healthy. That path has a
regression test: the first draft swallowed a 401 and printed "all monitors
fresh", reproducing inside the watcher the exact bug it exists to catch.

Thresholds are multiples of each monitor's own period, so one dropped
GitHub slot does not cry wolf. The clock and fetch are injected, so the
decision path is tested offline against a stubbed API — a monitor's tests
must not be hostage to the service it monitors.

Scope is the alerting only. The checkpoint re-seed and the cadence change
that would let a Rekor scan finish inside its interval both need a
maintainer's judgement, and stay open on #258.

Claim-issue: #258

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQNTis6LuJHd5KDXTvYG74

Copy link
Copy Markdown
Contributor Author

CI settled: 23 of 24 checks green. pr-claim passes, so the Claim-issue: trailer resolves against #258 correctly.

The one red check is osv / osv-scan, and it is not this PR's. Standing down on it rather than widening the diff, with the evidence:

What it reports — one package, four highs, all the same dependency:

| OSV URL                             | CVSS | PACKAGE  | VERSION | FIXED | SOURCE                                   |
| https://osv.dev/GHSA-5jgf-p345-68v8 | 7.5  | fast-uri | 3.1.5   | 3.1.6 | vendor/conformance-kit/package-lock.json |
| https://osv.dev/GHSA-f65p-4m7j-42xc | 7.5  | fast-uri | 3.1.5   | 3.1.6 | vendor/conformance-kit/package-lock.json |
| https://osv.dev/GHSA-fph4-wmhf-6fwf | 7.5  | fast-uri | 3.1.5   | 3.1.6 | vendor/conformance-kit/package-lock.json |
| https://osv.dev/GHSA-jqff-g426-hqxp | 7.5  | fast-uri | 3.1.5   | 3.1.6 | vendor/conformance-kit/package-lock.json |

Why it is not from this change:

  • This PR touches four files, none of them a lockfile and none under vendor/: the workflow, the script, its test, and one line of package.json. The script is dependency-free by design.
  • fast-uri@3.1.5 is already on main. git diff origin/main...HEAD -- vendor/ package-lock.json is empty, so both are byte-identical to the base.
  • The base commit has no recorded osv check run to compare against, so I verified the dependency directly on main rather than inferring it from a green base.

Why I am not porting a fix. The bump to 3.1.6 exists, but it lands in vendor/conformance-kit/package-lock.json, and that tree is hash-pinned:

$ node scripts/verify-vendor.mjs
✓ verify-vendor: vendor/conformance-kit/ matches the hash-pin (49 files @ 52e523460ee8…)

Editing the vendored lockfile here would turn verify-vendor red to turn osv-scan green, which is not a fix. The bump belongs upstream in conformance-kit, followed by a re-vendor and a pin bump in this repo. That is its own change with its own claim, not something to smuggle into a monitoring PR.

Worth flagging on its own terms though: four high-severity advisories against a pinned vendor tree is a real finding, and security.known-vulns on /conformance currently reads met — 0 known critical/high vulns. Those two statements disagree. The conformance evidence counts production dependencies and the scan reaches the vendored dev tree, which is a defensible scope difference, but it is worth someone deciding which scope the public claim means. Happy to file that separately if it is not already tracked.

Left as a draft. The diff is complete and tested; it needs a maintainer's read on the thresholds and on whether this belongs here or in bounded-systems/.github as a reusable.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants